﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IdentityModel.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Altairis.IdentityToolkit.Configuration;


namespace Altairis.IdentityToolkit.LiveId {

    /// <summary>
    /// Class representing the collection of Windows Live ID claims that are associated with an entity.
    /// </summary>
    public class LiveClaimSet : ClaimSet {
        private ServiceElement config;
        private byte[] decryptionKey, validationKey;
        private string plainToken;

        /// <summary>
        /// Initializes a new instance of the <see cref="LiveClaimSet"/> class.
        /// </summary>
        /// <param name="encryptedToken">The encrypted token.</param>
        public LiveClaimSet(string encryptedToken) {
            // Perform basic validation of token
            if (encryptedToken == null) throw new ArgumentNullException("stoken");
            if (string.IsNullOrEmpty(encryptedToken)) throw new ArgumentException("Value cannot be null or empty string.", "stoken");

            // Load configuration
            var liveConfig = ConfigurationManager.GetSection("altairis.identityToolkit/liveId") as LiveConfiguration;
            this.config = liveConfig.Service;

            // Get keys
            this.decryptionKey = this.GetKey("ENCRYPTION");
            this.validationKey = this.GetKey("SIGNATURE");

            // Decrypt token data
            try {
                this.DecryptToken(encryptedToken);
            }
            catch (Exception ex) {
                throw new TokenDecryptionException("Unable to decrypt token data.", ex);
            }

            // Validate signature
            this.ValidateToken();

            // Parse token fields
            this.ParseToken();

            // Populate ClaimSet
            this.PopulateClaims();
        }

        /// <summary>
        /// Gets or sets the application ID.
        /// </summary>
        /// <value>The application ID.</value>
        public string ApplicationId { get; private set; }

        /// <summary>
        /// Gets or sets the user unique ID.
        /// </summary>
        /// <value>The user unique ID.</value>
        public Guid UserUniqueId { get; private set; }

        /// <summary>
        /// Gets or sets the time when the originating ticket was generated.
        /// </summary>
        /// <value>The time when the originating ticket was generated.</value>
        public DateTime Timestamp { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether to use permanent cookie.
        /// </summary>
        /// <value><c>true</c> if web is requested to use permanent cookie; otherwise, <c>false</c>.</value>
        public bool UsePermanentCookie { get; private set; }

        private void PopulateClaims() {
            var cList = new List<Claim>();
            cList.Add(new Claim(LiveClaimTypes.UserUniqueId, this.UserUniqueId, Rights.Identity));
            cList.Add(new Claim(LiveClaimTypes.ApplicationId, this.ApplicationId, Rights.PossessProperty));
            cList.Add(new Claim(LiveClaimTypes.Timestamp, this.Timestamp, Rights.PossessProperty));
            cList.Add(new Claim(LiveClaimTypes.UsePermanentCookie, this.UsePermanentCookie, Rights.PossessProperty));
            this.cs = new DefaultClaimSet(cList);
        }

        private void ParseToken() {
            // Perform basic validation
            var fields = HttpUtility.ParseQueryString(plainToken);
            if (fields.Count < 3) throw new TokenParseException("Too few of token fields.");

            // Get user unique ID
            if (string.IsNullOrEmpty(fields["uid"])) throw new TokenParseException("Field 'uid' is missing or empty.");
            if (!fields["uid"].IsRegexMatch("^[0-9a-f]{32}$", RegexOptions.IgnoreCase)) throw new TokenParseException("Field 'uid' has invalid format.");
            this.UserUniqueId = new Guid(fields["uid"]);

            // Get application ID
            this.ApplicationId = fields["appid"];
            if (string.IsNullOrEmpty(this.ApplicationId)) throw new TokenParseException("Field 'appid' is missing or empty.");

            // Get timestamp
            int ts;
            if (string.IsNullOrEmpty(fields["ts"])) throw new TokenParseException("Field 'ts' is missing or empty.");
            if (!int.TryParse(fields["ts"], out ts)) throw new TokenParseException("Field 'ts' has invalid format (cannot be parsed as int).");
            this.Timestamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(ts);

            // Get flags
            int flags;
            if (string.IsNullOrEmpty(fields["flags"])) flags = 0;
            else if (!int.TryParse(fields["flags"], out flags)) throw new TokenParseException("Field 'flags' has invalid format (cannot be parsed as int).");
            this.UsePermanentCookie = (flags % 2) == 1;
        }

        private void ValidateToken() {
            // Split to data and signature
            string[] parts = this.plainToken.Split(new string[] { "&sig=" }, StringSplitOptions.None);
            if (parts.Length != 2) throw new TokenValidationException("Invalid format of token - can't get signature.");
            byte[] providedSig = DecodeBase64(parts[1]);

            // Get correct signature
            byte[] correctSig;
            using (HMACSHA256 algo = new HMACSHA256(this.validationKey))
                correctSig = algo.ComputeHash(Encoding.Default.GetBytes(parts[0]));

            // Validate signatures
            if (correctSig.Length != providedSig.Length) throw new TokenValidationException("Invalid signature length.");
            for (int i = 0; i < correctSig.Length; i++) {
                if (correctSig[i] != providedSig[i]) throw new TokenValidationException("Invalid signature.");
            }
        }

        private void DecryptToken(string stoken) {
            // Decode token
            byte[] tokenData = DecodeBase64(stoken);

            // Prepare for token decryption
            RijndaelManaged aes = new RijndaelManaged();
            aes.KeySize = 128;
            aes.Key = this.decryptionKey;
            aes.Padding = PaddingMode.PKCS7;
            using (var ms = new System.IO.MemoryStream(tokenData)) {
                // Read IV
                byte[] iv = new byte[16];
                ms.Read(iv, 0, 16);
                aes.IV = iv;

                // Decrypt data
                using (var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read))
                using (var sr = new System.IO.StreamReader(cs, Encoding.ASCII))
                    this.plainToken = sr.ReadToEnd();
            }
        }

        private byte[] GetKey(string prefix) {
            using (SHA256 algo = SHA256.Create()) {
                byte[] data = Encoding.Default.GetBytes(prefix + this.config.SecretKey);
                data = algo.ComputeHash(data);
                byte[] byteKey = new byte[16];
                Array.Copy(data, byteKey, 16);
                return byteKey;
            }
        }

        private static byte[] DecodeBase64(string s) {
            s = HttpUtility.UrlDecode(s);
            return Convert.FromBase64String(s);
        }

        private static string EncodeBase64(byte[] data) {
            string s = Convert.ToBase64String(data);
            return HttpUtility.UrlEncode(s);
        }

        #region ClaimSet abstract class implementation

        private DefaultClaimSet cs;

        /// <summary>
        /// Gets the number of claims in this claim set.
        /// </summary>
        /// <value></value>
        /// <returns>The number of claims in this <see cref="T:System.IdentityModel.Claims.ClaimSet"/>.</returns>
        public override int Count {
            get { return this.cs.Count; }
        }

        /// <summary>
        /// Searches for <see cref="T:System.IdentityModel.Claims.Claim"/> object that matches the specified claim type and rights in the <see cref="T:System.IdentityModel.Claims.ClaimSet"/>.
        /// </summary>
        /// <param name="claimType">The uniform resource identifier (URI) of a claim type. Several claim types are available as static properties of the <see cref="T:System.IdentityModel.Claims.ClaimTypes"/> class.</param>
        /// <param name="right">The URI of the right associated with the new claim. Several rights are available as static properties of the <see cref="T:System.IdentityModel.Claims.Rights"/> class.</param>
        /// <returns>
        /// A<see cref="T:System.Collections.Generic.IEnumerable`1"/> of type <see cref="T:System.IdentityModel.Claims.Claim"/> that enables you to enumerate the claims that matches the specified criteria.
        /// </returns>
        public override IEnumerable<Claim> FindClaims(string claimType, string right) {
            return this.cs.FindClaims(claimType, right);
        }

        /// <summary>
        /// Gets a <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to enumerate the <see cref="T:System.IdentityModel.Claims.Claim"/> object in the <see cref="T:System.IdentityModel.Claims.ClaimSet"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to enumerate the <see cref="T:System.IdentityModel.Claims.Claim"/> object in the <see cref="T:System.IdentityModel.Claims.ClaimSet"/>.
        /// </returns>
        public override IEnumerator<Claim> GetEnumerator() {
            return this.cs.GetEnumerator();
        }

        /// <summary>
        /// Gets the <see cref="System.IdentityModel.Claims.Claim"/> at the specified index.
        /// </summary>
        /// <value></value>
        public override Claim this[int index] {
            get { return this.cs[index]; }
        }

        /// <summary>
        /// Gets the entity that issued this <see cref="T:System.IdentityModel.Claims.ClaimSet"/>.
        /// </summary>
        /// <value></value>
        /// <returns>The <see cref="T:System.IdentityModel.Claims.ClaimSet"/> object that issued this <see cref="T:System.IdentityModel.Claims.ClaimSet"/> object.</returns>
        public override ClaimSet Issuer {
            get { return this; }
        }

        #endregion

    }

    /// <summary>
    /// Represents the types of claims that an entity can claim using Windows Live ID service. 
    /// </summary>
    public static class LiveClaimTypes {

        /// <summary>
        /// Gets the URI for a claim that specifies the user unique ID of an entity.
        /// </summary>
        /// <value>The URI for a claim that specifies the user unique ID of an entity..</value>
        /// <remarks>The string returned by this property is http://schemas.altairis.cz/IdentityToolkit/WindowsLiveID/Claims/UserUniqueId.</remarks>
        public static string UserUniqueId { get { return "http://schemas.altairis.cz/IdentityToolkit/WindowsLiveID/Claims/UserUniqueId"; } }

        /// <summary>
        /// Gets the URI for a claim that specifies the application id.
        /// </summary>
        /// <value>The URI for a claim that specifies the application id.</value>
        /// <remarks>The string returned by this property is http://schemas.altairis.cz/IdentityToolkit/WindowsLiveID/Claims/ApplicationId.</remarks>
        public static string ApplicationId { get { return "http://schemas.altairis.cz/IdentityToolkit/WindowsLiveID/Claims/ApplicationId"; } }

        /// <summary>
        /// Gets the URI for a claim that specifies the timestamp.
        /// </summary>
        /// <value>The URI for a claim that specifies the timestamp.</value>
        /// <remarks>The string returned by this property is http://schemas.altairis.cz/IdentityToolkit/WindowsLiveID/Claims/Timestamp.</remarks>
        public static string Timestamp { get { return "http://schemas.altairis.cz/IdentityToolkit/WindowsLiveID/Claims/Timestamp"; } }

        /// <summary>
        /// Gets the URI for a claim that specifies if permanent cookie should be used.
        /// </summary>
        /// <value>The the URI for a claim that specifies if permanent cookie should be used.</value>
        /// <remarks>The string returned by this property is http://schemas.altairis.cz/IdentityToolkit/WindowsLiveID/Claims/UsePermanentCookie.</remarks>
        public static string UsePermanentCookie { get { return "http://schemas.altairis.cz/IdentityToolkit/WindowsLiveID/Claims/UsePermanentCookie"; } }
    }


}
